import { ErrorDto } from './../error/error.dto';
import { ProjectModel } from './../project/project.model';
import { ChartService } from './../../providers/helper/helper.chart.service';
import { STAT_USER_NUM_INTERVAL } from './../../app.config';
import { ErrorModel } from './../error/error.model';
import { InjectRepository } from '@nestjs/typeorm';
import { InjectQueue } from 'nest-bull';
import { ProjectService } from './../project/project.service';
import {
  AddLogDto,
  LogDto,
  QueryStatLogDto,
  QueryLogListDto,
  LogListDto,
  LogSlsDto,
} from './search.dto';
import { IpService } from './../../providers/helper/helper.ip.service';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { Injectable } from '@nestjs/common';
import * as UA from 'ua-device';
import { UaService } from '@/providers/helper/helper.ua.service';
import { QueryListQuery, IPageData, QueryListQueryBySls } from '@/interfaces/request.interface';
import { Queue } from 'bull';
import * as uuidv4 from 'uuid/v4';
import {
  Repository,
  getConnection,
  Between,
  LessThan,
  MoreThan,
} from 'typeorm';
import { eachDay, format, addMonths, addYears } from 'date-fns';
import * as path from 'path';
import { ILogJobData } from './search.queue';
import { SlsService } from '@/providers/sls/sls.service';

@Injectable()
export class SearchService {
  constructor(
    private readonly elasticsearchService: ElasticsearchService,
    private readonly slsService: SlsService,
    @InjectRepository(ErrorModel)
    private readonly errorModel: Repository<ErrorModel>,
    @InjectRepository(ProjectModel)
    private readonly projectModel: Repository<ProjectModel>,
    private readonly ipService: IpService,
    private readonly projectService: ProjectService,
    private readonly uaService: UaService,
    private readonly chartService: ChartService,
    @InjectQueue()
    private readonly queue: Queue,
  ) { }

  private tolevel(data: any) {

    let content = [];
    for (const key in data) {
      if (data[key] !== undefined && data[key] !== null) {
        let value = data[key];
        if (typeof data[key] !== 'object') {
          value = String(value);
          if (content.findIndex(item => item.key === key) === -1) {
            content.push({
              key,
              value
            });
          }

        } else {
          if (data[key] instanceof Array) {
            value = JSON.stringify(value);
            if (content.findIndex(item => item.key === key) === -1) {
              content.push({
                key,
                value
              });
            }
          } else {
            content = content.concat(this.tolevel(value).filter(item => content.findIndex(con => con.key === item.key) === -1));
          }
        }
      }
    }

    return content;
  }

  private bulk(body: any, ua: any, uid: string): LogSlsDto[] {

    const bulk: LogSlsDto[] = [];
    const time = Math.floor(body.data.time / 1000);
    const data: LogSlsDto = {
      time,  // 单位秒
      contents: []
    };
    // tslint:disable-next-line:max-line-length
    // data.contents = this.tolevel({ eventId: uuidv4(), ...body.location, ...body.data, time, ...body.info, ...body.libInfo, behavior: body.behavior, ...ua, uid })
    data.contents = this.tolevel({ eventId: uuidv4(), uid, time, ...ua, ...body })
    console.log(data.contents);
    bulk.push(data);
    return bulk;
  }

  private escape(query: string) {
    return query.replace(/([-\!\*\+\&\|\(\)\[\]\{\}\^\~\?\:\"\/])/g, '\\$1');
  }

  async search(params: QueryListQueryBySls<QueryLogListDto>) {
    return await this.slsService.query(params);
  }

  async get<T>(params) {
    return await this.elasticsearchService.getClient().get(params);
  }

  async create<T>(params) {
    return await this.elasticsearchService.getClient().bulk({
      index: 'string',
      type: 'string',
      body: 'any',
    });
  }

  async update(params: any) {
    return await this.slsService.getEventNumAndUserNum(params);
  }
  /**
   *添加处理日志队列
   *
   * @param {*} { body, ip, ua, uid }
   * @returns
   * @memberof SearchService
   */
  public dealLogByQueue({ body, ip, ua, uid }) {
    const data = JSON.parse(body); //JSON.parse(Buffer.from(body, 'base64').toString());
    this.queue.add('dealLog', {
      body: data,
      ip: ip.replace(/[^.\d]/g, ''),
      ua,
      uid,
    });
    return;
  }

  /**
   *队列日志处理函数
   *解析ip 和 ua
   * @param {ILogJobData} { body, ip, ua, uid }
   * @returns
   * @memberof SearchService
   */
  public async dealLog({ body, ip, ua, uid }: ILogJobData) {
    const location = await this.ipService.query(ip);
    body.location = location;
    const uaDetail = this.uaService.parse(ua);
    const bulk: LogSlsDto[] = this.bulk(body, uaDetail, uid);
    this.saveLogToSls(bulk);
    return bulk;
  }

  private getErrors(bulks: any[]): any[] {
    const errorMap: { [prop: string]: { data: any, num: number } } = this.bulks.reduce((total, item) => {
      if (item.contents) {
        const data: any = item.contents.reduce((t, i) => ({ ...t, [i.key]: i.value }), {});
        if (total[data.errorId]) {
          total[data.errorId].num++;
        } else {
          total[data.errorId] = { ...data, num: 1 };
        }
      }
      return total;
    }, {});
    const result: ErrorDto[] = Object.values(errorMap).map(({ projectId, ...item }: any) => ({
      id: item.errorId,
      project: { id: Number(projectId) },
      ...item,
      eventNum: item.num
    }))
    return result
  }
  private time = Date.now();
  private bulks: LogSlsDto[] = [];
  private saveLogToSls(bulk: LogSlsDto[]) {
    this.bulks = this.bulks.concat(bulk);
    if (this.time < Date.now() - 10 * 1000 || this.bulks.length > 10) {
      this.time = Date.now();
      const errorDatas = this.getErrors(this.bulks);
      this.queue.add('updateError', errorDatas);
      this.createLog(this.bulks);
      this.bulks = [];
    }
  }

  public async createLog(bulk: LogSlsDto[]) {
    return await this.slsService.create(bulk);
  }


  public async getLogList<T>(
    query: QueryListQueryBySls<QueryLogListDto>
  ): Promise<IPageData<LogListDto>> {
    const { list, totalCount }: any = await this.search(query);

    let source;
    const sameStack = {};
    for (let item of list) {
      // const stack = item.stack && JSON.parse(item.stack);
      const stack = item.stack;
      if (!stack || (stack && !stack[0])) {
        continue;
      }
      const key = `${stack[0].url}-${stack[0].line}-${stack[0].column}-${item.version}`;
      if (!!sameStack[key]) {
        source = sameStack[key];
      } else {
        source = await this.projectService.getSourceCode(
          stack[0],
          item.projectId,
          item.version,
        );
        sameStack[key] = source;
      }
      item.source = source;
    }

    return {
      totalCount,
      list
    }
  }

  public async addComputedErrorsUserNumTask() {
    const errors = await this.errorModel.find({
      where: {
        updatedAt: Between(
          new Date(Date.now() - STAT_USER_NUM_INTERVAL - 8 * 60 * 60 * 1000),
          new Date(Date.now() - 8 * 60 * 60 * 1000),
        ),
      },
    });
    // const errors = await this.errorModel.find();
    errors.forEach(item => {
      this.queue.add('countUserNum', item);
    });
  }

  public async computedErrorsUserNum(error) {
    const data = await this.update({ projectId: error.projectId, errorId: error.id })
    if (data) {
      error.userNum = data.userNum;
      error.eventNum = data.eventNum;
      await this.errorModel
        .query(
          `UPDATE error_model set userNum = ${error.userNum}, eventNum = ${error.eventNum} where id = "${error.id}"`, //防止更新更新时间
        )
        .catch(e => {
          // console.log(e);
        });
      console.log(
        `UPDATE error_model set userNum = ${error.userNum} where id = ${error.id}`,
      );
    }

    return true;
  }

  private getTimeFlag(startDate, endDate) {
    let dayNum = (endDate - startDate) / 3600000 / 24;
    let type = 'YYYY-MM-DD', flag = 'day';
    if (dayNum > 365) {
      type = 'YYYY';
      flag = 'year';
    } else if (dayNum > 31) {
      type = 'YYYY-MM';
      flag = 'month';
    } else {
      let days = eachDay(new Date(startDate), new Date(endDate));
      if (days.length === 1) {
        flag = 'hour';
        type = 'HH:mm';
      }
    }

    return { type, flag };

  }

  public async statLog(query: QueryStatLogDto) {

    const { endDate, startDate, projectId, errorId } = query;

    const { flag, type } = this.getTimeFlag(startDate, endDate);
    const queryData = {
      projectId,
      errorId,
      from: Math.floor(startDate / 1000),
      to: Math.floor(endDate / 1000)
    }
    // tslint:disable-next-line:max-line-length
    const [trendStat, osStat, browserStat, deviceStat] = await Promise.all([this.slsService.queryNumByTime({ ...queryData, flag }), this.slsService.queryCategory({ ...queryData, flag: 'os' }), this.slsService.queryCategory({ ...queryData, flag: 'browser' }), this.slsService.queryCategory({ ...queryData, flag: 'deviceType' })])
    return {
      trendStat,
      osStat,
      browserStat,
      deviceStat
    }

  }

  public async statError(query: any) {
    const { endDate, startDate, projectId } = query;

    const { flag, type } = this.getTimeFlag(startDate, endDate);
    const data = await this.slsService.queryNumByTime({
      projectId,
      from: Math.floor(startDate / 1000),
      to: Math.floor(endDate / 1000),
      flag
    })

    return data
  }

  /**
   * addGenerateImgTask
   */
  public async addGenerateImgTask() {
    const errors = await this.errorModel.find({
      where: {
        updatedAt: Between(new Date(Date.now() - 24 * 3600 * 1000), new Date()),
      },
    });

    const projectIds = errors.map(item => item.projectId);

    projectIds.forEach(item => {
      this.queue.add('generateImg', item);
    });
  }

  private getChartOption(data) {
    return {
      grid: {
        left: 10,
        top: 10,
        bottom: 50,
        right: 10,
      },
      xAxis: {
        type: 'category',
        boundaryGap: ['20%', '20%'],
        splitLine: false,
        axisLine: {
          lineStyle: {
            color: '#999',
          },
        },
        data: data.data.map(item => item.date),
      },
      yAxis: {
        show: false,
        type: 'value',
        splitLine: false,
      },
      series: [
        {
          data: data.data.map(item => item.count),
          type: 'line',
          lineStyle: {
            color: '#09c',
          },
          areaStyle: {
            color: '#09c',
          },
          smooth: true,
        },
      ],
    };
  }

  public async generateImg(projectId) {
    const data = await this.statError({
      endDate: Date.now(),
      startDate: Date.now() - 86400000 * 7,
      projectId,
    });

    const image = this.chartService.generateImg({
      option: this.getChartOption(data),
      path: path.join(
        __dirname,
        `../../publics/charts/${projectId}_${Date.now()}.png`,
      ),
    });

    const project = await this.projectModel.findOne(projectId);
    project.image = image;
    this.projectModel.save(project);
    return true;
  }
}
